Skip to content

Fix order-dependent deletion for superset value lists (#260) - #263

Merged
fym-rgb merged 1 commit into
aws:mainfrom
Pybsama:codex/fix-superset-value-delete
Aug 29, 2026
Merged

Fix order-dependent deletion for superset value lists (#260)#263
fym-rgb merged 1 commit into
aws:mainfrom
Pybsama:codex/fix-superset-value-delete

Conversation

@Pybsama

@Pybsama Pybsama commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Issue #, if available:

Closes #260

Description of changes:

Machine.deleteRule could silently fail when the supplied rule contained a
superset of the stored values. Values within a field have OR semantics, but
the delete path reused a mutable candidate set across those alternatives and
also used an empty set to mean both "not initialized yet" and "no candidates
matched the preceding keys." The result depended on value order.

This change:

  • tracks candidate-set initialization explicitly;
  • evaluates every value/NameState alternative from the same candidates inherited
    from preceding keys;
  • prevents an empty candidate set at a later key from being reinitialized; and
  • limits an empty branch to that branch instead of skipping the remaining
    alternatives for the field.

The existing multi-NameState wildcard teardown guard remains unchanged.

Testing:

  • Added order-independent reproductions for exact and wildcard value lists.
  • Added prior-key and terminal boundary tests to prevent candidate reinitialization.
  • Added coverage for deleting independent sub-rules with the same rule name.
  • The five focused regressions passed in 20 consecutive runs.
  • mvn -Dtest=MachineTest test: 91 tests passed.
  • mvn verify: 772 tests passed; Checkstyle and SpotBugs reported no findings.

Benchmark / Performance (for source code changes):

Environment:

  • Apple Silicon, macOS 26.5.1
  • OpenJDK 21.0.12
  • Maven 3.9.16

Full comparison command:

./scripts/perf-compare.sh \
  337e8d99026d3f6687f783535f157be8bf4ebbb7 \
  0e0b36545d52b71be24355349a0ce7f314706db0 \
  -- -Dstyle.color=never -Druler.perf.warmup=5 -Druler.perf.measure=10
rule_type                     before_eps     after_eps   delta_pct  verdict
---------                     ----------     ---------   ---------  -------
EXACT                             431543        428359       -0.7%  noise (±2.8%)
WILDCARD                          276394        341981      +23.7%  improvement (noise ±3.2%)
PREFIX                            433777        429462       -1.0%  noise (±2.1%)
PREFIX_EIC                        424932        429335       +1.0%  noise (±9.5%)
SUFFIX                            422192        425125       +0.7%  noise (±4.2%)
SUFFIX_EIC                        421414        422822       +0.3%  noise (±2.3%)
EQUALS_IGNORE_CASE                380035        382514       +0.7%  noise (±2.7%)
NUMERIC                           279480        281772       +0.8%  noise (±1.3%)
ANYTHING_BUT                      250883        242854       -3.2%  REGRESSION (noise ±3.0%)
ANYTHING_BUT_IGNORE_CASE          243198        245030       +0.8%  noise (±2.4%)
ANYTHING_BUT_PREFIX               254955        259433       +1.8%  noise (±2.6%)
ANYTHING_BUT_SUFFIX               254234        252456       -0.7%  noise (±2.6%)
ANYTHING_BUT_WILDCARD             231962        282701      +21.9%  improvement (noise ±1.9%)
COMPLEX_ARRAYS                     55038         54984       -0.1%  noise (±1.7%)

The only full-run flag was ANYTHING_BUT, 0.2 percentage points outside the
script's heuristic noise band. I repeated the whole ANYTHING_BUT family with
5 warmup and 15 measurement passes:

rule_type                     before_eps     after_eps   delta_pct  verdict
---------                     ----------     ---------   ---------  -------
ANYTHING_BUT                      254288        235802       -7.3%  noise (±17.4%)
ANYTHING_BUT_IGNORE_CASE          242591        245506       +1.2%  noise (±1.9%)
ANYTHING_BUT_PREFIX               255447        258124       +1.0%  noise (±7.0%)
ANYTHING_BUT_SUFFIX               251671        251312       -0.1%  noise (±4.5%)
ANYTHING_BUT_WILDCARD             232983        234265       +0.6%  noise (±3.1%)

No matching-throughput regression was reproducible. This change only affects
rule deletion; the benchmark exercises event matching.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@fym-rgb

fym-rgb commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Hi @Pybsama
Thank you for the PR, will review the content in the coming days.

@fym-rgb fym-rgb self-assigned this Aug 21, 2026
@fym-rgb

fym-rgb commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Thanks @Pybsama for your contribution. This is a careful fix, and the test design is strong.

  • Your five tests cherry-picked onto unmodified main fail 4/5, exactly as they should (the 5th, testIssue260SupersetDeletePreservesPriorKeyCandidates, correctly passes on main — it pins behavior a naive fix would break).
  • testIssue260TerminalSupersetDoesNotResetPriorKeyCandidates is a nice catch beyond what Machine.deleteRule with a superset value list can silently no-op depending on value order #260 documented: on main, the empty-set re-init at a terminal key over-deletes — it removes registrations reachable only via other first-key values. Your fix closes both directions.
  • Full mvn verify on your branch is green (772 tests), and the five new tests are stable across 20 repeated runs.
  • The approach matches the direction we settled in Fix Machine.deleteRule isolation for shared wildcard sub-patterns (#255) #256: values within a key are OR semantics, so every value/NameState alternative is evaluated from the candidates inherited from the preceding keys. Unifying the single- and multi-NameState paths is a welcome simplification, and I confirmed the wildcard teardown guard is unchanged and nothing reachable from the matching path is touched (so your no-benchmark-regression conclusion holds).

Two changes I'd like before merge:

  1. Simplify the new flag away. At every call site candidateSubRuleIdsInitialized equals keyIndex != 0 (deletePatternRule passes (0, false); the only recursion passes (keyIndex + 1, true)). Carrying the same fact twice through the walk invites drift at a future call site. Please drop the parameter from both signatures and test keyIndex == 0 at the initialization branch — zero behavior change, smaller diff. While you're there: the new HashSet<>(candidateSubRuleIds) at the recursion call is now a redundant defensive copy (post-change, deleteStep never mutates its parameter — each branch takes its own copy), so it can pass the set directly.

  2. Pin the later-key continuation with a test that actually deletes. The "empty branch is confined to that branch" behavior is only exercised-with-deletion at the first key. Both later-key tests use delete lists that cover no stored sub-rule, so they can't distinguish "continued past the empty branch" from "aborted the key". Please add a variant where the second value at a later key legitimately deletes, e.g.:

    • add r1 {"a":["one"],"b":["right"]} and other {"a":["one"],"b":["missing"]}
    • delete r1 with {"a":["one"],"b":["missing","right"]} in both value orders
    • assert r1 is gone, other still matches, and the machine is empty after deleting other.

Optionally polish these:

  • A short comment on why withAdditionalNameStateReuse(true) is load-bearing in the two later-key tests (it funnels a key's values into one shared NameState, which is what makes the foreign patterns findable there).
  • The new comment's "rather than consuming another branch's result" describes the old behavior; ending at the positive statement reads cleaner.
  • A sentence in the deletePatternRule javadoc stating the now-guaranteed semantics (values that reach no rule are skipped; the outcome doesn't depend on value order within a key) — Machine.deleteRule with a superset value list can silently no-op depending on value order #260 existed exactly because this was ambiguous.
  • The trailing-blank-line removal at the end of GenericMachine.java is unrelated to the fix; restoring it keeps the diff scoped.

When you've made the changes, please mark the PR ready for review and we can get this formally reviewed.


Generative AI Assisted Review

@Pybsama
Pybsama force-pushed the codex/fix-superset-value-delete branch from 0e0b365 to d8e4af7 Compare August 28, 2026 15:44
@Pybsama
Pybsama marked this pull request as ready for review August 28, 2026 15:44
@Pybsama

Pybsama commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the requested changes and marked the PR ready for review:

  • Rebased the single fix commit onto current main (a356c6f).
  • Removed candidateSubRuleIdsInitialized, derived initialization from keyIndex == 0, and passed the candidate set directly into recursion.
  • Added the later-key deletion regression for both [missing,right] and [right,missing], preserving the other rule and confirming final machine teardown.
  • Added the NameState-reuse rationale, documented order-independent value semantics, made the branch comment positive, and restored the unrelated trailing blank line.

Fresh verification on JDK 21:

  • Issue260 tests: 6 passed
  • MachineTest: 98 passed
  • mvn verify: 779 passed; Checkstyle and SpotBugs clean
  • Benchmarks#CL2*: 2 passed

Current head: d8e4af7.

@fym-rgb fym-rgb left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Pybsama.

Approving. It's been a pleasure reviewing this, thank you for the contribution.

@fym-rgb
fym-rgb merged commit c53050e into aws:main Aug 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Machine.deleteRule with a superset value list can silently no-op depending on value order

2 participants